package com.thoughtworks.impersonator.http.record;

import java.net.InetSocketAddress;

import org.apache.log4j.Logger;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineCoverage;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpRequestEncoder;
import org.jboss.netty.handler.codec.http.HttpResponseDecoder;

import com.thoughtworks.impersonator.http.message.storage.HttpExchangeStorage;

@ChannelPipelineCoverage("one")
class ClientConnectionHandler extends SimpleChannelUpstreamHandler {
    private final int serverPort;
    private final String serverHost;
    private final HttpExchangeStorage exchangeStorage;
    private final ClientBootstrap httpClient;
    private volatile Channel serverConnection;
    private static final Logger log = Logger.getLogger(ClientConnectionHandler.class);

    ClientConnectionHandler(ClientBootstrap httpClient, HttpExchangeStorage exchangeStorage, String serverHost,
            int serverPort) {
        this.exchangeStorage = exchangeStorage;
        this.serverHost = serverHost;
        this.serverPort = serverPort;
        this.httpClient = httpClient;
    }

    @Override
    public void channelOpen(ChannelHandlerContext context, ChannelStateEvent event) throws Exception {
        // Suspend incoming traffic until connected to the remote host.
        final Channel clientConnection = event.getChannel();
        clientConnection.setReadable(false);

        setupProxyConnection(context, clientConnection);
        
        ChannelFuture futureConnection = httpClient.connect(new InetSocketAddress(serverHost, serverPort));

        serverConnection = futureConnection.getChannel();
        futureConnection.addListener(new ChannelFutureListener() {
            public void operationComplete(ChannelFuture futureServerConnection) throws Exception {
                if (futureServerConnection.isSuccess()) {
                    clientConnection.setReadable(true);
                } else {
                    clientConnection.close();
                }
            }
        });
    }

    private void setupProxyConnection(ChannelHandlerContext context, Channel channel) {
        ChannelPipeline pipeline = httpClient.getPipeline();
        pipeline.addLast("decoder", new HttpResponseDecoder());
        pipeline.addLast("chunkAggregator", new HttpChunkAggregator(1048576));
        pipeline.addLast("handler", new ServerConnectionHandler(context, channel, exchangeStorage));
        pipeline.addLast("encoder", new HttpRequestEncoder());
    }

    @Override
    public void messageReceived(ChannelHandlerContext context, MessageEvent event) throws Exception {
        HttpRequest request = (HttpRequest) event.getMessage();
        context.setAttachment(request);
        request.setHeader(HttpHeaders.Names.HOST, serverHost);
        serverConnection.write(request);
    }

    @Override
    public void channelClosed(ChannelHandlerContext context, ChannelStateEvent event) throws Exception {
        close(serverConnection);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext context, ExceptionEvent event) throws Exception {
        log.error("unexpected exception caught on client handler, closing connection", event.getCause());
        Channel clientConnection = event.getChannel();
        close(clientConnection);
    }

    private void close(Channel channel) {
        if (channel != null && channel.isOpen() && channel.isConnected()) {
            channel.write(ChannelBuffers.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
        }
    }

}
